package fquery

import (
	"fmt"
	"sort"
	"strconv"
	"strings"

	"github.com/starter-go/afs"
)

// ParseLocation 把字符串解析为 Location
func ParseLocation(str string) (*Location, error) {
	p := &myLocationParser{}
	return p.parse(str)
}

////////////////////////////////////////////////////////////////////////////////

const (
	qNameHost = "host"
	qNameMask = "mask"
)

// Location 表示要查询的位置；
// （URI）& (BaseAt+PathElements) 应该至少有一组参数；
// 如果都为空，则返回默认位置；
// 如果都有赋值，则按照 (BaseAt+PathElements) 的位置查询;
type Location struct {
	MaskLength int               `json:"mask"`     // 基础 URI (aka.mask) 包含的 elements 数量
	HostIndex  int               `json:"host"`     // 主机信息 (user@hostname:port)在 elements 中的 index
	Elements   []string          `json:"elements"` // 解析后的各个部分
	Query      map[string]string `json:"query"`    // 查询参数
}

// URI ...
func (inst *Location) URI() afs.URI {
	str := inst.String()
	return afs.URI(str)
}

func (inst *Location) String() string {
	le := &myLocationElements{
		parent: inst,
	}

	q := inst.Query
	if q == nil {
		q = make(map[string]string)
	}
	q[qNameMask] = le.int2string(inst.MaskLength)
	q[qNameHost] = le.int2string(inst.HostIndex)

	str1 := le.stringify(inst.Elements)
	str2 := le.query2string(q)
	return str1 + str2
}

// Normalize  标准化这个 Location
func (inst *Location) Normalize() (*Location, error) {
	src := inst
	le := &myLocationElements{parent: inst}
	elist, err := le.normalize(src.Elements)
	if err != nil {
		return nil, err
	}
	dst := &Location{}
	*dst = *src
	dst.Elements = elist
	return dst, nil
}

// IsAbs 判断这个 Location 是否是一个绝对位置 (绝对 URI)
func (inst *Location) IsAbs() bool {
	elist := inst.Elements
	size := len(elist)
	if size > 0 {
		el0 := elist[0]
		el0size := len(el0)
		return strings.HasSuffix(el0, ":") && (el0size > 2)
	}
	return false
}

// IsEmpty 判断这个 Location 是否为空
func (inst *Location) IsEmpty() bool {
	l1 := len(inst.Elements)
	l2 := len(inst.Query)
	return (l1 + l2) < 1
}

// HasSameMask 判断是否具有相同的 mask
func (inst *Location) HasSameMask(other *Location) bool {

	if inst == nil || other == nil {
		return false
	}

	if inst.MaskLength != other.MaskLength {
		return false
	}

	mlen := inst.MaskLength
	elen1 := len(inst.Elements)
	elen2 := len(other.Elements)
	if elen1 < mlen || elen2 < mlen {
		return false
	}

	for i := 0; i < mlen; i++ {
		e1 := inst.Elements[i]
		e2 := other.Elements[i]
		if e1 != e2 {
			return false
		}
	}

	return true
}

// SetQueryParam ...
func (inst *Location) SetQueryParam(name, value string) {
	t := inst.Query
	if t == nil {
		t = make(map[string]string)
		inst.Query = t
	}
	t[name] = value
}

////////////////////////////////////////////////////////////////////////////////

type myLocationParser struct {
}

func (inst *myLocationParser) parse(str string) (*Location, error) {

	result := &Location{}
	p0, p1 := inst.splitToElementsAndQuery(str)

	// split to elements
	elist, err := inst.parseElements(p0)
	if err != nil {
		return nil, err
	}
	result.Elements = elist

	// query
	query, err := inst.parseQuery(p1)
	if err != nil {
		return nil, err
	}
	result.Query = query

	// others
	hasHostInfo := inst.checkHasHostInfo(str)
	result.HostIndex = inst.readHostIndex(query, hasHostInfo)
	result.MaskLength = inst.readMaskLen(query, hasHostInfo)

	// normalize
	result2, _ := result.Normalize()
	if result2 == nil {
		result2 = result
	}
	return result2, nil
}

func (inst *myLocationParser) readHostIndex(q map[string]string, hasHostInfo bool) int {
	if q != nil {
		str := q[qNameHost]
		n, err := strconv.Atoi(str)
		if err == nil && n > 0 {
			return n
		}
	}
	// as default
	if hasHostInfo {
		return 1 // like 'http://hostname/'
	}
	return -1 // like 'file:/'
}

func (inst *myLocationParser) readMaskLen(q map[string]string, hasHostInfo bool) int {
	if q != nil {
		str := q[qNameMask]
		n, err := strconv.Atoi(str)
		if err == nil && n > 0 {
			return n
		}
	}
	// as default
	if hasHostInfo {
		return 2 // like 'http://hostname/'
	}
	return 1 // like 'file:/'
}

func (inst *myLocationParser) splitToElementsAndQuery(str string) (string, string) {
	const n = 2
	parts := strings.SplitN(str, "?", n)
	if len(parts) == n {
		return parts[0], parts[1]
	}
	return str, ""
}

func (inst *myLocationParser) checkHasHostInfo(uri string) bool {
	const (
		p1 = ":/"
		p2 = "://"
		p3 = ":///"
	)
	i1 := strings.Index(uri, p1)
	i2 := strings.Index(uri, p2)
	i3 := strings.Index(uri, p3)
	return i1 == i2 && i1 != i3 && i1 > 0
}

func (inst *myLocationParser) parseQuery(qstr string) (map[string]string, error) {
	if strings.HasPrefix(qstr, "?") {
		qstr = qstr[1:]
	}
	src := strings.Split(qstr, "&")
	dst := make(map[string]string)
	for _, item := range src {
		const count = 2
		kv := strings.SplitN(item, "=", count)
		if len(kv) == count {
			name := kv[0]
			value := kv[1]
			dst[name] = value
		}
	}
	return dst, nil
}

func (inst *myLocationParser) parseElements(str string) ([]string, error) {
	le := &myLocationElements{}
	list := le.parse(str)
	return list, nil
}

////////////////////////////////////////////////////////////////////////////////

type myLocationElements struct {
	parent *Location
}

func (inst *myLocationElements) stringify(elements []string) string {
	ihost := inst.parent.HostIndex
	builder := &strings.Builder{}
	count := 0
	for i, el := range elements {
		if i == 0 {
			// NOP
		} else if i > ihost {
			builder.WriteString("/")
		} else if i == ihost {
			builder.WriteString("//")
		}
		builder.WriteString(el)
		count++
	}
	if count == 1 {
		builder.WriteString("/")
	}
	str := builder.String()
	if strings.HasSuffix(str, ":") {
		str = str + "/"
	}
	return str
}

func (inst *myLocationElements) parse(str string) []string {
	const (
		sep = "/"
		aka = "\\"
	)
	if strings.Contains(str, aka) {
		str = strings.ReplaceAll(str, aka, sep)
	}
	return strings.Split(str, sep)
}

func (inst *myLocationElements) normalize(elements []string) ([]string, error) {
	src := elements
	dst := make([]string, 0)
	for _, el := range src {
		el2 := strings.TrimSpace(el)
		if el2 == "" {
			continue
		} else if el2 == "." {
			continue
		} else if el2 == ".." {
			err := inst.checkBaseLength(dst)
			if err != nil {
				return nil, err
			}
			dst = inst.popEnding(dst)
		} else {
			dst = append(dst, el)
		}
	}
	return dst, nil
}

func (inst *myLocationElements) checkBaseLength(elements []string) error {
	size := len(elements)
	min := 1
	if size < min {
		return fmt.Errorf("out of base path length")
	}
	return nil
}

func (inst *myLocationElements) popEnding(elements []string) []string {
	size := len(elements)
	if size > 0 {
		return elements[0 : size-1]
	}
	return elements
}

func (inst *myLocationElements) int2string(n int) string {
	return strconv.Itoa(n)
}

func (inst *myLocationElements) query2string(query map[string]string) string {
	builder := &strings.Builder{}
	keys := make([]string, 0)
	for k := range query {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for i, key := range keys {
		value := query[key]
		if i == 0 {
			builder.WriteRune('?')
		} else {
			builder.WriteRune('&')
		}
		builder.WriteString(key)
		builder.WriteRune('=')
		builder.WriteString(value)
	}
	return builder.String()
}

////////////////////////////////////////////////////////////////////////////////
